/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.filters;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * Provides a single configuration point for security measures that required the
 * addition of one or more HTTP headers to the response.
 */
public class HttpHeaderSecurityFilter extends FilterBase {

	private static final Log log = LogFactory.getLog(HttpHeaderSecurityFilter.class);

	// HSTS
	private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
	// Click-jacking protection
	private static final String ANTI_CLICK_JACKING_HEADER_NAME = "X-Frame-Options";
	// Block content sniffing
	private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME = "X-Content-Type-Options";
	private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE = "nosniff";
	// Cross-site scripting filter protection
	private static final String XSS_PROTECTION_HEADER_NAME = "X-XSS-Protection";
	private static final String XSS_PROTECTION_HEADER_VALUE = "1; mode=block";
	private boolean hstsEnabled = true;
	private int hstsMaxAgeSeconds = 0;
	private boolean hstsIncludeSubDomains = false;
	private boolean hstsPreload = false;
	private String hstsHeaderValue;
	private boolean antiClickJackingEnabled = true;
	private XFrameOption antiClickJackingOption = XFrameOption.DENY;
	private URI antiClickJackingUri;
	private String antiClickJackingHeaderValue;
	private boolean blockContentTypeSniffingEnabled = true;
	private boolean xssProtectionEnabled = true;

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		super.init(filterConfig);

		// Build HSTS header value
		StringBuilder hstsValue = new StringBuilder("max-age=");
		hstsValue.append(hstsMaxAgeSeconds);
		if (hstsIncludeSubDomains) {
			hstsValue.append(";includeSubDomains");
		}
		if (hstsPreload) {
			hstsValue.append(";preload");
		}
		hstsHeaderValue = hstsValue.toString();

		// Anti click-jacking
		StringBuilder cjValue = new StringBuilder(antiClickJackingOption.headerValue);
		if (antiClickJackingOption == XFrameOption.ALLOW_FROM) {
			cjValue.append(' ');
			cjValue.append(antiClickJackingUri);
		}
		antiClickJackingHeaderValue = cjValue.toString();
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
	                     FilterChain chain) throws IOException, ServletException {

		if (response instanceof HttpServletResponse) {
			HttpServletResponse httpResponse = (HttpServletResponse) response;

			if (response.isCommitted()) {
				throw new ServletException(sm.getString("httpHeaderSecurityFilter.committed"));
			}

			// HSTS
			if (hstsEnabled && request.isSecure()) {
				httpResponse.setHeader(HSTS_HEADER_NAME, hstsHeaderValue);
			}

			// anti click-jacking
			if (antiClickJackingEnabled) {
				httpResponse.setHeader(ANTI_CLICK_JACKING_HEADER_NAME, antiClickJackingHeaderValue);
			}

			// Block content type sniffing
			if (blockContentTypeSniffingEnabled) {
				httpResponse.setHeader(BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME,
						BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE);
			}

			// cross-site scripting filter protection
			if (xssProtectionEnabled) {
				httpResponse.setHeader(XSS_PROTECTION_HEADER_NAME, XSS_PROTECTION_HEADER_VALUE);
			}
		}

		chain.doFilter(request, response);
	}

	@Override
	protected Log getLogger() {
		return log;
	}

	@Override
	protected boolean isConfigProblemFatal() {
		// This filter is security related to configuration issues always
		// trigger a failure.
		return true;
	}

	public boolean isHstsEnabled() {
		return hstsEnabled;
	}

	public void setHstsEnabled(boolean hstsEnabled) {
		this.hstsEnabled = hstsEnabled;
	}

	public int getHstsMaxAgeSeconds() {
		return hstsMaxAgeSeconds;
	}

	public void setHstsMaxAgeSeconds(int hstsMaxAgeSeconds) {
		if (hstsMaxAgeSeconds < 0) {
			this.hstsMaxAgeSeconds = 0;
		} else {
			this.hstsMaxAgeSeconds = hstsMaxAgeSeconds;
		}
	}

	public boolean isHstsIncludeSubDomains() {
		return hstsIncludeSubDomains;
	}

	public void setHstsIncludeSubDomains(boolean hstsIncludeSubDomains) {
		this.hstsIncludeSubDomains = hstsIncludeSubDomains;
	}

	public boolean isHstsPreload() {
		return hstsPreload;
	}

	public void setHstsPreload(boolean hstsPreload) {
		this.hstsPreload = hstsPreload;
	}

	public boolean isAntiClickJackingEnabled() {
		return antiClickJackingEnabled;
	}

	public void setAntiClickJackingEnabled(boolean antiClickJackingEnabled) {
		this.antiClickJackingEnabled = antiClickJackingEnabled;
	}

	public String getAntiClickJackingOption() {
		return antiClickJackingOption.toString();
	}

	public void setAntiClickJackingOption(String antiClickJackingOption) {
		for (XFrameOption option : XFrameOption.values()) {
			if (option.getHeaderValue().equalsIgnoreCase(antiClickJackingOption)) {
				this.antiClickJackingOption = option;
				return;
			}
		}
		throw new IllegalArgumentException(
				sm.getString("httpHeaderSecurityFilter.clickjack.invalid", antiClickJackingOption));
	}

	public String getAntiClickJackingUri() {
		return antiClickJackingUri.toString();
	}

	public void setAntiClickJackingUri(String antiClickJackingUri) {
		URI uri;
		try {
			uri = new URI(antiClickJackingUri);
		} catch (URISyntaxException e) {
			throw new IllegalArgumentException(e);
		}
		this.antiClickJackingUri = uri;
	}

	public boolean isBlockContentTypeSniffingEnabled() {
		return blockContentTypeSniffingEnabled;
	}

	public void setBlockContentTypeSniffingEnabled(
			boolean blockContentTypeSniffingEnabled) {
		this.blockContentTypeSniffingEnabled = blockContentTypeSniffingEnabled;
	}

	public boolean isXssProtectionEnabled() {
		return xssProtectionEnabled;
	}

	public void setXssProtectionEnabled(boolean xssProtectionEnabled) {
		this.xssProtectionEnabled = xssProtectionEnabled;
	}

	private static enum XFrameOption {
		DENY("DENY"),
		SAME_ORIGIN("SAMEORIGIN"),
		ALLOW_FROM("ALLOW-FROM");

		private final String headerValue;

		private XFrameOption(String headerValue) {
			this.headerValue = headerValue;
		}

		public String getHeaderValue() {
			return headerValue;
		}
	}
}
